import { QdrantClient, Schemas } from "@qdrant/js-client-rest";
import { createHash } from "crypto";
import path from "path";
import { IVectorStore, PointStruct, VectorStoreSearchResult, Payload } from "../interfaces/index.js";
import { DEFAULT_MAX_SEARCH_RESULTS, DEFAULT_SEARCH_MIN_SCORE } from "../constants/index.js";
import { DocumentType } from "../utils/instruction-manager.js";

export class QdrantVectorStore implements IVectorStore {
  private readonly vectorDimensions: { code: number; text: number };
  private readonly DISTANCE_METRIC = "Cosine";

  private client: QdrantClient;
  private readonly workspaceId: string;
  private readonly qdrantUrl: string;

  constructor(
    workspacePath: string,
    url: string,
    vectorDimensions: { code: number; text: number },
    apiKey?: string
  ) {
    const parsedUrl = this.parseQdrantUrl(url);
    this.qdrantUrl = parsedUrl;

    try {
      const urlObj = new URL(parsedUrl);
      let port: number;
      let useHttps: boolean;

      if (urlObj.port) {
        port = Number(urlObj.port);
        useHttps = urlObj.protocol === "https:";
      } else {
        useHttps = urlObj.protocol === "https:";
        port = useHttps ? 443 : 80;
      }

      this.client = new QdrantClient({
        host: urlObj.hostname,
        https: useHttps,
        port: port,
        prefix: urlObj.pathname === "/" ? undefined : urlObj.pathname.replace(/\/+$/, ""),
        apiKey,
        headers: { "User-Agent": "Codebase-RAG-MCP" },
      });
    } catch (urlError) {
      this.client = new QdrantClient({
        url: parsedUrl,
        apiKey,
        headers: { "User-Agent": "Codebase-RAG-MCP" },
      });
    }

    const hash = createHash("sha256").update(workspacePath).digest("hex");
    this.workspaceId = `ws-${hash.substring(0, 16)}`;
    this.vectorDimensions = vectorDimensions;
  }

  private getCollectionName(docType: DocumentType): string {
    return `${this.workspaceId}_${docType}`;
  }

  private parseQdrantUrl(url: string | undefined): string {
    if (!url || url.trim() === "") return "http://localhost:6333";
    const trimmedUrl = url.trim();
    if (!trimmedUrl.startsWith("http://") && !trimmedUrl.startsWith("https://") && !trimmedUrl.includes("://")) {
      return this.parseHostname(trimmedUrl);
    }
    try {
      new URL(trimmedUrl);
      return trimmedUrl;
    } catch {
      return this.parseHostname(trimmedUrl);
    }
  }

  private parseHostname(hostname: string): string {
    return hostname.includes(":") ? `http://${hostname}` : `http://${hostname}`;
  }

  private async getCollectionInfo(docType: DocumentType): Promise<Schemas["CollectionInfo"] | null> {
    try {
      return await this.client.getCollection(this.getCollectionName(docType));
    } catch (error) {
      return null;
    }
  }

  async initialize(): Promise<boolean> {
    const codeCreated = await this._initializeCollection("code");
    const textCreated = await this._initializeCollection("text");
    return codeCreated || textCreated;
  }

  private async _initializeCollection(docType: DocumentType): Promise<boolean> {
    const collectionName = this.getCollectionName(docType);
    const vectorSize = docType === "code" ? this.vectorDimensions.code : this.vectorDimensions.text;
    
    try {
      const collectionInfo = await this.getCollectionInfo(docType);

      if (collectionInfo === null) {
        await this.client.createCollection(collectionName, {
          vectors: { size: vectorSize, distance: this.DISTANCE_METRIC },
        });
        await this._createPayloadIndexes(docType);
        return true;
      }

      const vectorsConfig = collectionInfo.config?.params?.vectors as Schemas['VectorParams'];
      if (vectorsConfig?.size !== vectorSize) {
        return await this._recreateCollectionWithNewDimension(docType, vectorsConfig?.size || 0);
      }
      
      await this._createPayloadIndexes(docType);
      return false;
    } catch (error) {
      console.error(`[Qdrant] Failed to initialize collection "${collectionName}":`, error);
      throw new Error(`Failed to connect to Qdrant at ${this.qdrantUrl}`);
    }
  }

  private async _recreateCollectionWithNewDimension(docType: DocumentType, existingSize: number): Promise<boolean> {
    const collectionName = this.getCollectionName(docType);
    const vectorSize = docType === "code" ? this.vectorDimensions.code : this.vectorDimensions.text;
    console.warn(`[Qdrant] Collection ${collectionName} exists with size ${existingSize}, but expected ${vectorSize}. Recreating.`);
    await this.client.deleteCollection(collectionName);
    await new Promise((resolve) => setTimeout(resolve, 100));
    await this.client.createCollection(collectionName, {
      vectors: { size: vectorSize, distance: this.DISTANCE_METRIC },
    });
    return true;
  }

  private async _createPayloadIndexes(docType: DocumentType): Promise<void> {
    const collectionName = this.getCollectionName(docType);
    for (let i = 0; i <= 4; i++) {
      try {
        await this.client.createPayloadIndex(collectionName, {
          field_name: `pathSegments.${i}`,
          field_schema: "keyword",
        });
      } catch (error: any) {
        if (!error.message?.includes("already exists")) {
          console.warn(`[Qdrant] Could not create payload index for ${collectionName}:`, error);
        }
      }
    }
  }

  async upsertPoints(points: PointStruct[], docType: DocumentType): Promise<void> {
    const collectionName = this.getCollectionName(docType);
    const processedPoints = points.map((point) => {
      const segments = point.payload.filePath.split(path.sep).filter(Boolean);
      const pathSegments = segments.reduce((acc: Record<string, string>, segment: string, index: number) => {
        acc[index.toString()] = segment;
        return acc;
      }, {});
      return { ...point, payload: { ...point.payload, pathSegments } };
    });
    await this.client.upsert(collectionName, { points: processedPoints, wait: true });
  }

  private isPayloadValid(payload: Record<string, unknown> | null | undefined): payload is Payload {
    return !!payload && ["filePath", "codeChunk", "startLine", "endLine"].every(key => key in payload);
  }

  async search(
    queryVector: number[],
    docType: DocumentType,
    directoryPrefix?: string,
    minScore?: number,
    maxResults?: number
  ): Promise<VectorStoreSearchResult[]> {
    const collectionName = this.getCollectionName(docType);
    let filter: Schemas['Filter'] | undefined;
    if (directoryPrefix) {
      const segments = directoryPrefix.split(path.sep).filter(Boolean);
      filter = {
        must: segments.map((segment, index) => ({
          key: `pathSegments.${index}`,
          match: { value: segment },
        })),
      };
    }

    const result = await this.client.query(collectionName, {
      query: queryVector,
      filter,
      score_threshold: minScore ?? DEFAULT_SEARCH_MIN_SCORE,
      limit: maxResults ?? DEFAULT_MAX_SEARCH_RESULTS,
      with_payload: { include: ["filePath", "codeChunk", "startLine", "endLine"] },
    });

    return result.points.filter(p => this.isPayloadValid(p.payload)) as VectorStoreSearchResult[];
  }

  async deletePointsByMultipleFilePaths(filePaths: string[], docType: DocumentType): Promise<void> {
    if (filePaths.length === 0) return;
    const collectionName = this.getCollectionName(docType);
    await this.client.delete(collectionName, {
      filter: {
        should: filePaths.map(fp => ({ key: "filePath", match: { value: fp } })),
      },
      wait: true,
    });
  }

  async deleteCollection(): Promise<void> {
    await this._deleteCollection("code");
    await this._deleteCollection("text");
  }

  private async _deleteCollection(docType: DocumentType): Promise<void> {
    const collectionName = this.getCollectionName(docType);
    if (await this.collectionExists(docType)) {
      await this.client.deleteCollection(collectionName);
    }
  }

  async collectionExists(docType: DocumentType): Promise<boolean> {
    return !!await this.getCollectionInfo(docType);
  }
}
